package io.gitee.devaper.starlight.security

import io.gitee.devaper.starlight.security.event.*
import org.springframework.beans.factory.InitializingBean
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.ApplicationEventPublisherAware
import org.springframework.security.core.session.*
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy
import org.springframework.stereotype.Component

/**
 * 发布事件的并发会话认证策略, 使可以立刻对被登出的用户执行一些操作, 而不是等到下次访问
 *
 * 原本的ConcurrentSessionControlAuthenticationStrategy支持将用户登出, 但是对于WS连接等不会断开, 但是也不会发送登出事件,
 *      这个实现类是对于它的一个扩展, 用来统计本次被登出的用户, 并且发布事件供其它模块使用做一些资源清理操作
 */
@Component
open class PublishEventConcurrentSessionControlAuthenticationStrategy(sessionRegistry: SessionRegistry) :
    ConcurrentSessionControlAuthenticationStrategy(sessionRegistry),
    ApplicationEventPublisherAware,
    InitializingBean
{

    override fun afterPropertiesSet() {
        setMaximumSessions(1)
        setExceptionIfMaximumExceeded(false)
    }

    override fun allowableSessionsExceeded(sessions: MutableList<SessionInformation>, allowableSessions: Int, registry: SessionRegistry) {
        // 记录未过期的Session
        val nonExpiredSet = sessions.asSequence()
            .filter { !it.isExpired }
            .toHashSet()

        try {
            // 原始处理方法
            super.allowableSessionsExceeded(sessions, allowableSessions, registry)
        } finally {
            // 检查过期了哪些Session并且发布事件
            sessions.forEach { session ->
                // 如果是刚刚被登出的用户, 那么发布强制登出事件
                if (session.isExpired && nonExpiredSet.contains(session)) {
                    applicationEventPublisher.publishEvent(ConcurrentSessionControlLogoutEvent(session.principal))
                }
            }
        }
    }

    private lateinit var applicationEventPublisher: ApplicationEventPublisher

    override fun setApplicationEventPublisher(applicationEventPublisher: ApplicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher
    }

}